﻿namespace MonetDbDataProvider.Mapi
{
    using System;
    using System.IO;
    using System.Text;

    /// <summary>
    /// Much of this class was created from an older (2008) open source .NET MonetDb driver.
    /// https://code.google.com/p/monetdb-net/source/browse/trunk/MonetDb/MapiSocket.cs
    /// </summary>
    public class MonetDbBlockStream : Stream
    {
        private readonly byte[] readBlock, writeBlock;

        private Stream monetDbStream;

        private int readPosition, writePosition, readLength, writeLength;

        private bool isLastReadBlock;

        public MonetDbBlockStream(Stream monetDbStream)
        {
            this.monetDbStream = new BufferedStream(monetDbStream);
            this.readBlock = new byte[Int16.MaxValue + 3];
            this.writeBlock = new byte[Int16.MaxValue];
            this.isLastReadBlock = false;
        }

        public override void Flush()
        {
            this.WriteNextBlock(true);
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new System.NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new System.NotImplementedException();
        }
        
        public override int Read(byte[] buffer, int offset, int count)
        {
            if ((offset + count) > buffer.Length)
                throw new ArgumentOutOfRangeException("offset", "Offset and count are greater than the buffer.");

            var returnValue = 0;
            var available = this.readLength - this.readPosition;

            if (available == 0) available = this.ReadNextBlock();

            while (available > 0 && returnValue < count)
            {
                var length = count - returnValue > available ? available : count - returnValue;
                Array.Copy(this.readBlock, this.readPosition, buffer, offset, length);
                returnValue += length;
                offset += length;
                this.readPosition += length;
                available = this.readLength - this.readPosition;
                if (!this.isLastReadBlock && available == 0)
                    available = this.ReadNextBlock();
            }

            return returnValue;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (offset + count > buffer.Length)
                throw new ArgumentOutOfRangeException("offset", "offset + count cannot be greater than the buffer length");

            while (count > 0)
            {
                if (count < this.writeBlock.Length - this.writePosition)
                {
                    Array.Copy(buffer, offset, this.writeBlock, this.writePosition, count);
                    this.writeLength += count;
                    count = 0;
                }
                else
                {
                    int tempCount = this.writeBlock.Length - this.writePosition;
                    Array.Copy(buffer, offset, this.writeBlock, this.writePosition, tempCount);
                    offset += tempCount;
                    count -= tempCount;
                    this.writeLength += tempCount;
                    this.WriteNextBlock(false);
                }
            }
        }

        public override bool CanRead
        {
            get
            {
                return this.monetDbStream.CanRead;
            }
        }

        public override bool CanSeek
        {
            get
            {
                return this.monetDbStream.CanSeek;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return this.monetDbStream.CanWrite;
            }
        }

        public override long Length
        {
            get
            {
                return this.monetDbStream.Length;
            }
        }

        public override long Position
        {
            get
            {
                return this.monetDbStream.Position;
            }

            set
            {
                this.monetDbStream.Position = value;
            }
        }

        private int ReadNextBlock()
        {
            var blockHeader = new byte[2];
            var read = 0;

            if (this.monetDbStream.Read(blockHeader, 0, blockHeader.Length) != 2)
                throw new InvalidDataException("Invalid block size returned from server.");

            this.readLength = (blockHeader[0] & 0xFF) >> 1 | (((short)blockHeader[1] & 0xFF) << 7);
            this.isLastReadBlock = (blockHeader[0] & 0x1) == 1;

            while (read < this.readLength)
                read += this.monetDbStream.Read(this.readBlock, read, this.readLength - read);

            this.readPosition = 0;

            if (this.isLastReadBlock)
            {
                if (this.readLength > 0 && this.readBlock[this.readLength - 1] != '\n')
                    this.readBlock[this.readLength++] = (byte)'\n';

                this.readBlock[this.readLength++] = Encoding.UTF8.GetBytes(LineTypes.Prompt)[0];
                this.readBlock[this.readLength++] = (byte)'\n';
            }

            return this.readLength - this.readPosition;
        }

        private void WriteNextBlock(bool last)
        {
            var blockHeader = new byte[2];
            var blockSize = (Int16)this.writeLength;

            if (last)
            {
                blockHeader[0] = (byte)(blockSize << 1 & 0xFF | 1);
                blockHeader[1] = (byte)(blockSize >> 7);
            }
            else
            {
                blockHeader[0] = (byte)(blockSize << 1 & 0xFF);
                blockHeader[1] = (byte)(blockSize >> 7);
            }

            this.monetDbStream.Write(blockHeader, 0, blockHeader.Length);
            this.monetDbStream.Write(this.writeBlock, 0, this.writeLength);
            this.monetDbStream.Flush();
            this.writePosition = 0;
            this.writeLength = 0;
        }
    }
}
